-- CommonTrapM
-- Created by panyl
-- 通用陷阱模块

module("CommonTrapM", package.seeall);

local directions = {
    ["left"]  = { -1, 0, }, -- 向左
    ["right"] = { 1, 0, },  -- 向右
    ["down"]  = { 0, 1, }, -- 向下
    ["up"]    = { 0, -1, },  -- 向上
};

-- 所有陷阱信息
local traps = {};

-- 子模块
local rules = {};

-- 载入配置表
local _init = false;
local loadCsv = function()
    if _init then return end
    _init = true;

    -- 转换下trap信息表
    local trap_CSV = get_trap_CSV();
    for i = 1, #trap_CSV do
        traps[trap_CSV[i].id] = trap_CSV[i];
    end
    trap_CSV = {};
end

function init()
    if not _init then
        loadCsv();

        -- 载入所有的规则处理子模块
        rules = LOAD_PATH("game/logic/module/traps");
    end

    -- 关注回合数变化
    CombatM.registerStartRoundCB("common_trap_start_round", whenCombatRound);
end

-- 回合数变化
function whenCombatRound()
    checkTimingTrap();
end

-- 检索
function query(trapId, path)
    local m = traps[trapId];

    if not m or not path then
        return m;
    end

    return m[path];
end

-- 陷阱剩余次数
function getRemainTimes(trap)
    local grid;
    if type(trap) == "table" then
        grid = trap;
    elseif type(trap) == "number" then
        grid = DungeonM.getGridByPos(trap);
    else
        return 0;
    end

    local usedTimes = grid.times or 0;
    local maxTimes = query(grid.id, "times");

    if maxTimes == -1 then
        return MAX_VALUE;
    end

    return math.max(maxTimes - usedTimes, 0);
end

-- 获取方向向量
function getVector(direction)
    return directions[direction];
end

-- 获取某个方向上怪物的数量
function getMonsterCount(pos, direction)
    local coor = DungeonM.convertToCoor(pos);
    local x = coor.x;
    local y = coor.y;
    local count = 0;
    -- 因为这时候格子还未生成，所有这里用原始的数据来判断
    local data = DungeonM.getGrids();
    local vector = getVector(direction);

    while true
    do
        x = x + vector[1];
        y = y + vector[2];

        -- 超出了
        if x <= 0 or x > DUNGEON_WIDTH or y <= 0 or y > DUNGEON_HEIGHT then
            break;
        end

        pos = DungeonM.convertToPos(x, y);

        local class = table.keys(data[pos])[1];
        local trapType = DungeonM.query(class, "type");

        -- 目前只有GRID_TYPE_MONSTER、GRID_TYPE_BOX是怪物
        if trapType == GRID_TYPE_MONSTER or trapType == GRID_TYPE_BOX then
            count = count + 1;
        end
    end

    return count;
end

-- 计算某个方向上的格子数量
function calcGridCount(pos, direction)
    local coor = DungeonM.convertToCoor(pos);
    local x = coor.x;
    local y = coor.y;

    if direction == "left" then
        return x - 1;
    elseif direction == "right" then
        return DUNGEON_WIDTH - x;
    elseif direction == "up" then
        return y - 1;
    else
        return DUNGEON_HEIGHT - y;
    end
end

-- 抽取方向
function fetchTrapDirection(trapId, pos)
    local directionCount;
    local count = 0;
    local fourDirections = { "left", "right", "up", "down", };

    -- TODO: 有两边相等时需要随机方向
    local arr = {};

    for index = 1, #fourDirections do
        local direction = fourDirections[index];
        directionCount = getMonsterCount(pos, direction);

        if count < directionCount then
            count = directionCount;
            arr = { direction };
        elseif count == directionCount then
            table.insert(arr, direction);
        end
    end

    local direction;

    -- 随机数
    if count > 0 then
        local rand = DungeonM.getRandSeed("trap_direction");
        direction = arr[ rand % #arr + 1 ];
    else
        -- 四个方向都没有怪物，取格子数量最大的方向
        count = 0;
        for index = 1, #fourDirections do
            local total = calcGridCount(pos, fourDirections[index]);
            if count < total then
                count = total;
                direction = fourDirections[index];
            end
        end
    end

    DungeonLogM.addLog(string.format("陷阱(%d/%d)的方向为%s", trapId, pos, direction));

    return direction;
end

-- 获取陷阱数据
function fetchTrapData(trapId, pos, times)
    local ret = {
        ["times"] = times or 0,
        ["id"] = trapId,
    };

    -- 方向
    local trapType = query(trapId, "type");
    if trapType == TRAP_TYPE_BEELINE then
        ret["direction"] = fetchTrapDirection(trapId, pos);
    elseif trapType == TRAP_TYPE_CANNON then
        -- 大炮
        ret["direction"] = "up";
    elseif trapType == TRAP_TYPE_UP then
        ret["direction"] = "up";
    elseif trapType == TRAP_TYPE_DOWN then
        ret["direction"] = "down";
    elseif trapType == TRAP_TYPE_LEFT then
        ret["direction"] = "left";
    elseif trapType == TRAP_TYPE_RIGHT then
        ret["direction"] = "right";
    end

    return ret;
end

-- 检查是否能触发陷阱
function checkAction(trapId, pos, targetPos)
    local info = query(trapId);
    if not info then
        trace("CommonTrapM", "没有编号为%s的陷阱。", tostring(trapId));
        return false;
    end

    local grid = DungeonM.getGridByPos(pos);
    if grid.type ~= GRID_TYPE_TRAP then
        trace("CommonTrapM", "格子%d不是陷阱，type = %d。", pos, grid.type);
        return false;
    end
    if grid.state == GRID_STATE_DISABLE then
        trace("CommonTrapM", "格子%d已经失效了。", pos);
        return false;
    end

    local maxTimes = info["times"] or 0;
    local curTimes = grid.times or 0;
    if maxTimes ~= NUMBER_INFINITY and curTimes >= maxTimes then
        trace("CommonTrapM", "陷阱(%d/%d)最大使用次数为%d，已经使用过%d次，不能再次使用。",
            trapId, pos, maxTimes, curTimes);
        return false;
    end

    -- 有些陷阱发动需要消耗
    local cost = query(trapId, "cost") or {};
    local ok = true;
    for key, amount in pairs(cost) do
        if type(key) == "number" then
            -- 物品
            ok = ItemM.getAmount(ME.user, key) >= amount;
        elseif type(key) == "string" then
            -- 属性
            ok = ME.user.dbase:query(key, 0) >= amount;
        end

        if not ok then
            trace("CommonTrapM", "消耗不足，无法发动陷阱%d。", trapId);
            return ok;
        end
    end

    local trapType = info["type"];
    local feature = info["feature"];

    --需要手动选择目标的，或者是选择最大/最小血量的（因为可能服务器/客户端两边血量不一致，所以这两种也是用的客户端指定的目标）
    if  trapType == TRAP_TYPE_TARGET then
        if not targetPos or type(targetPos) ~= "number" then
            trace("CommonTrapM", "陷阱%d需要指定目标。", trapId);
            return false;
        end

        local g = DungeonM.getGridByPos(targetPos);
        if not g:isOpened() or not g:isMonster() then
            trace("CommonTrapM", "陷阱%d指定的目标必须是翻开的怪物。", trapId);
            return false;
        end
    end

    -- 暂时没有其他条件
    return true;
end

-- 消耗
function doCost(cost)
    for key, amount in pairs(cost) do
        if type(key) == "number" then
            ItemM.costAmount(ME.user, key, amount);
        elseif type(key) == "string" then
            ME.user:costAttrib(key, amount);
        end
    end
end

-- 掉落
function trapDropProperty(pos, trapId)
    -- 如果有掉落
    local dbase = query(trapId, "dbase") or {};
    local rate = dbase["drop_rate"];
    local alias = query(trapId, "alias");

    if dbase["drop"] and rate then
        local arr = table.deepcopy(dbase["drop"]);

        -- 如果是第一次掉落，第一个个掉落是100%
        if CombatStatM.getFieldStat(alias) <= 0 then
            rate = 100;
        end

        local rand = DungeonM.getRandSeed("trap_drop") % 100;
        local grid = DungeonM.getGridByPos(pos);
        if rand < rate then
            grid.bonus = table.deepcopy(dbase["drop"]);
        end

        CombatStatM.addFieldStat(alias, 1);
    end
end

-- 触发陷阱
function doTrapAction(pos, targetPos)
    local grid = DungeonM.getGridByPos(pos);
    local trapId = grid.id;

    if not trapId then
        return false;
    end

    -- 判断下能否触发该陷阱
    if not checkAction(trapId, pos, targetPos) then
        return false;
    end

    -- 先消耗
    local cost = query(trapId, "cost") or {};

    -- 概率不消耗
    if not PropM.trigger(ME.user, "probability_no_consume", trapId) then
        doCost(cost);
    end

    -- 先修改次数和状态
    grid.times = grid.times + 1;
    if getRemainTimes(pos) <= 0 then
        grid:changeState(GRID_STATE_DISABLE);

        -- 尝试掉落
        trapDropProperty(pos, trapId);
    end

    local direction = grid.direction;

    -- 1. 先对自己起效
    local damage = getEffect(trapId, "self_damage") or 0;
    if damage > 0 then
        -- 对玩家伤害
        CombatM.receiveDamage(ME.user, ME.user, damage, 0);
    end

    -- 2. 获取目标，并起效

    -- 统计存活的怪物数量
    local aliveMonsters = FormulaM.getAliveMonsters(true);
    local aliveCount = #table.keys(aliveMonsters);

    local targets = getTrapTargets(trapId, pos, targetPos);
    damage = getEffect(trapId, "damage");
    for _, targetGrid in pairs(targets) do
        repeat
            if not targetGrid:isOpened() then
                -- 开启格子，并指定延迟播放
                -- 需要等到炮弹经过时才翻开
                targetGrid:open(false, true);
            end

            local monster = targetGrid.monster;

            -- 不是怪物
            if not monster then
                break;
            end

            trace("CommonTrapM", "目标格子(%d)接收延迟伤害", targetGrid:getPos());

            -- 延迟伤害
            if SkillM.getSequence(monster) == nil then
                SkillM.getSequenceList()[monster] = CastSequence.create(ME.user, monster, 0);
            end

            -- 传入攻击者信息
            local assailantInfo = {
                ["grid_type"] = GRID_TYPE_TRAP,
                ["trap_id"]   = trapId,
            };

            SkillM.getSequence(monster):delay(grid, monster, 0);
            CombatM.receiveDamage(ME.user, monster, damage, 0, { assailantInfo = assailantInfo,} );
            EventMgr.fire(event.NEED_REFRESH_ATTRIB, {});

            -- 做一些额外效果
            doExtraApply(monster, trapId);
        until true;
    end

    -- 统计击杀的怪物数量
    aliveMonsters = FormulaM.getAliveMonsters(true);
    local killCount = aliveCount - #table.keys(aliveMonsters);

    -- 抛出发动陷阱事件
    EventMgr.fire(event.LAUNCH_TRAP, { ["trapId"] = trapId, ["pos"] = pos,
        ["targets"] = targets, ["direction"] = direction, ["killCount"] = killCount,});

    if DungeonM.isCostRound(grid) then
        -- 抛出一个回合事件
        -- 这里需要指定延迟播放，等攻击动画播放完毕后再更新血量
        EventMgr.fire(event.COMBAT_ROUND, { ["pos"] = pos, ["isDelay"] = true, } );
    end

    -- 通知建筑事件完成
    EventMgr.fire(event.BUILDING_EVENT_DONE, {["pos"] = pos,});

    return true;
end

-- 获取陷阱的作用目标，这里就不做详细的检查了
function getTrapTargets(trapId, pos, targetPos)
    local rule = query(trapId, "rule");
    local mod = rules[rule];

    return mod.getTargets(trapId, pos, targetPos);
end

-- 获取陷阱某个效果
function getEffect(trapId, key)
    local args = query(trapId, key);

    local formula;
    if type(args) == "string" and string.startWith(args, "formula_") then
        formula = string.sub(args, string.len("formula_") + 1);
        return FormulaM.invoke(formula, trapId, key);
    end

    return FormulaM.invoke("CALC_COMMON_TRAP_EFFECT", trapId, key, args);
end

-- 陷阱的额外效果
function doExtraApply(target, trapId)
    local args = getEffect(trapId, "apply");
    local round = CombatM.getRound();

    for key, info in pairs(args) do
        local value = table.copy(info); -- 复制一次，以免后面被修改掉

        if key == "skill" then
            -- 施放技能
            local assailantInfo = {
                ["grid_type"] = GRID_TYPE_TRAP,
                ["trap_id"]   = trapId,
            };
            SkillM.apply(ME.user, target, value["skill_id"], value["round"], { assailantInfo = assailantInfo });
        elseif key == "debuff" then
            if value[4] ~= -1 then
                -- 回合数
                value[4] = value[4] + round;
            end
            PropM.recordDebuff(target, value);
        elseif key == "buff" then
            if value[4] ~= -1 then
                value[4] = value[4] + round;
            end
            PropM.recordBuff(target, value);
        elseif "prop" then
            if value[4] ~= -1 then
                value[4] = value[4] + round;
            end
            PropM.record(target, value, "mixed");
        end
    end
end

-- 获取各个方向上目标位置（按从近到远排序）
function fetchDirectionPos(pos, grids)
    local ret = {};
    for direction, _ in pairs(directions) do
        -- 先初始化为空
        ret[direction] = {};
    end

    for _, grid in pairs(grids) do
        local direction = getTargetDirection(pos, grid:getPos());

        table.insert(ret[direction], grid);
    end

    local compareFunc = function(a, b)
        return calcDistance(pos, a:getPos()) < calcDistance(pos, b:getPos());
    end

    for direction, _ in pairs(ret) do
        table.sort(ret[direction], compareFunc);
    end

    return ret;
end

-- 获取目标方向，距离
function getTargetDirection(pos, targetPos)
    local coor = DungeonM.convertToCoor(pos);
    local x = coor["x"];
    local y = coor["y"];
    local targetCoor = DungeonM.convertToCoor(targetPos);
    local tx = targetCoor["x"];
    local ty = targetCoor["y"];

    if x == tx then
        -- 在同一条竖线上
        return iif(y < ty, "down", "up");
    elseif y == ty then
        -- 在同一条横线上
        return iif(x < tx, "right", "left");
    end
end

-- 两点距离
function calcDistance(pos1, pos2)
    local coor1 = DungeonM.convertToCoor(pos1);
    local x1 = coor1["x"];
    local y1 = coor1["y"];
    local coor2 = DungeonM.convertToCoor(pos2);
    local x2 = coor2["x"];
    local y2 = coor2["y"];

    return math.abs(x1 - x2) + math.abs(y1 - y2);
end

-- 获取方向
function getDirections()
    return { "up", "down", "right", "left" };
end

----------------------------------
-- 获取指定方向上最远的格子位置
-- @param refPos    参考格子位置
-- @param direction 方向
function getFarmostGridPos(refPos, direction)
    local refCoord = DungeonM.convertToCoor(refPos);

    local pos;
    if direction == "up" then
        pos = DungeonM.convertToPos(refCoord.x, 1);
    elseif direction == "down" then
        pos = DungeonM.convertToPos(refCoord.x, GRID_ROW_NUM);
    elseif direction == "left" then
        pos = DungeonM.convertToPos(1, refCoord.y);
    elseif direction == "right" then
        pos = DungeonM.convertToPos(GRID_COL_NUM, refCoord.y);
    else
        trace("CommonTrapM", "未知的方向：%s", tostring(direction));
    end

    return pos;
end

-------------------------
-- 获取指定方向上的所有格子
-- @param refPos    参考格子位置（陷阱位置）
-- @param direction 方向
function getDirGrids(refPos, direction)
    local refCoord = DungeonM.convertToCoor(refPos);
    local ret = {};

    if direction == "up" then
        if refCoord.y == 1 then
            -- 炮口被堵住了
            return {};
        end

        for i = refCoord.y - 1, 1, -1 do
            local pos = DungeonM.convertToPos(refCoord.x, i);
            table.insert(ret, DungeonM.getGridByPos(pos));
        end

    elseif direction == "down" then
        if refCoord.y == GRID_ROW_NUM then
            -- 炮口被堵住了
            return {};
        end

        for i = refCoord.y + 1, GRID_ROW_NUM do
            local pos = DungeonM.convertToPos(refCoord.x, i);
            table.insert(ret, DungeonM.getGridByPos(pos));
        end

    elseif direction == "left" then
        if refCoord.x == 1 then
            -- 炮口被堵住了
            return {};
        end

        for i = refCoord.x - 1, 1, -1 do
            local pos = DungeonM.convertToPos(i, refCoord.y);
            table.insert(ret, DungeonM.getGridByPos(pos));
        end

    elseif direction == "right" then
        if refCoord.x == GRID_COL_NUM then
            -- 炮口被堵住了
            return {};
        end

        for i = refCoord.x + 1, GRID_COL_NUM do
            local pos = DungeonM.convertToPos(i, refCoord.y);
            table.insert(ret, DungeonM.getGridByPos(pos));
        end

    else
        trace("CommonTrapM", "未知的方向：%s", tostring(direction));
    end

    return ret;
end

-- 是否是排炮
function isCannon(grid)
    if grid.type ~= GRID_TYPE_TRAP then
        return false;
    end

    local trapId = grid.id;
    local trapType = CommonTrapM.query(trapId, "type");

    return trapType == TRAP_TYPE_CANNON;
end

-- 尝试自动触发陷阱
function checkTimingTrap()
    local round = CombatM.getRound();
    local targetGrids = getTimingTrapGrids();

    for _, grid in pairs(targetGrids) do
        -- 尝试触发
        local trapId = grid.id;
        local cd = query(trapId, "cd");
        local openRound = grid.openRound;
        if round - openRound >= cd then
            doTrapAction(grid.pos)
        end
    end
end

-- 获取定时类陷阱的格子
function getTimingTrapGrids()
    local ret = {};

    -- 先取出所有格子
    local grids = DungeonM.getCurrentDungeon() or {};
    local trapId;
    for _, grid in pairs(grids) do
        if grid.state ~= GRID_STATE_DISABLE and grid.type == GRID_TYPE_TRAP then
            -- 所有配置了倒计时的陷阱
            trapId = grid.id;
            if type(query(trapId, "cd")) == 'number' then
                table.insert(ret, grid);
            end
        end
    end

    return ret;
end

-- 获取定时类陷阱的倒计时
function getTrapCountDown(pos)
    local grid = DungeonM.getGridByPos(pos);

    local cd = query(grid.id, "cd");
    if type(cd) ~= 'number' then
        return;
    end

    local round = CombatM.getRound();
    local openRound = grid.openRound;
    return cd - (round - openRound) + 1;
end
